<?php

/* Version: 
   Check and tune session support
   
   various kind of checking:
   - session failover (when not using local)
   - db fail over
   - memcache failover
   - add log to critical application error
     (kvcache local file open fail)

   20100819
   some basic functions are ready.
   20101202
   Unified some change, remove 2nd parameter in f:func feature
   fix a bug on errorlog $msg variable
   fix a bug on add_to_table_multiple_row
   20101208:
     added f:string 
   20101218: fixed a bug on process_select_result for undfined $pagelist_obj  
             fixed a bug on process_select_result $a_id_column_selected
   
   A-Z breakdown
   http://www.ihwy.com/labs/demos/current/jquery-listmenu-plugin.aspx
   
   Form validator:
   http://www.position-absolute.com/articles/jquery-form-validator-because-form-validation-is-a-mess/
   http://www.position-relative.net/creation/formValidator/indexOverflown.html
   http://www.position-relative.net/creation/formValidator/#
   
   http://www.phpbench.com/
   
   
*/





	
class CGIApplication extends Cgiapp{
	
//	# $db_connection_count to be removed
//  private $db_connection_count =0 ;    # Members declared as private may only be accessed by the class that defines the member.
//                                   # this counter count the number of connect & disconnect. it should be 1 or 0 if this application is properly written
//                                   
//  # to be removed                             
//  var $_DB_CONNECTION_POOL=array();
  
  protected $_DB_OBJECT = array(); # to be inherited by Child, note, using is_set to check 
  
  var $_PATH_INFO_POSITION_MAPPGING = array(); #path info name to position mapping  
  protected $debug_sql = 1; #Members declared protected can be accessed only within the class itself and by inherited and parent classes. 
   
  
  
  /*********************************************************
   * CGI Application compatibility
   *********************************************************/
	function __construct($args = null){
	  parent::__construct($args);
	  //header('Content-Type: text/html; charset=big5');
	  //$this->header_props(array( 'Content-Type'=>'text/plain'));

#header("Cache-Control: no-store, no-cache, must-revalidate");  // HTTP/1.1
#header("Cache-Control: post-check=0, pre-check=0", false);
#header("Pragma: no-cache");                                    // HTTP/1.0

	  $this->header_props(array( 'charset'=>'UTF-8'));
	  date_default_timezone_set('Asia/Hong_Kong'); // time zone need to set for date function
  }

  /* Some standard setup for each application
   */ 
  function setup(){
	  // move the 'site / project'
	  $this->tmpl_use_memcache = $this->param('TMPL_USE_MEMCACHE');
	  $this->param('SELF_URL',      '/'.$this->param('this_cgi'));
	  $this->param('SELF_FULL_URL', $this->param('CGI_URL').$this->param('this_cgi'));
	  
	  #if (! $this->tmpl_use_memcache){ // cannot stop this?? why
	    $this->tmpl_path($this->param('TMPL_CACHE_PATH').'Smarty'); //fix me, only relative path
    #}
	  // Use the exception_handler() method to handle run mode errors
	  $this->error_mode('exception_handler');

	  // Set the mode parameter to 'action'
	  $this->mode_param(array('path_info' => 1, 'param' => 'action'));
	  $this->init_session();
  }

  function init_session(){
    # Note: session is not used. 	
  	$session_obk = new Session();
  }



  /* Load template for this app by action
   * the template path is combined by app path + action name
   */
  function load_action_tmpl($action=null){
  	if ($action == null) $action = $this->get_current_runmode();
  	$h_tmpl    = $this->param('tmpl');
  	$base_path = $h_tmpl['base_path'];
  	$tmpl = array_key_exists($action, $h_tmpl) ? $h_tmpl[$action] : $base_path.$action.'.html'; // use base path + action as relative path
  	#return $this->load_tmpl($this->param('TMPL'), $tmpl);
  	return $this->load_tmpl($tmpl);
  }

  function is_mobile_device()
  {
    $user_agent       = $_SERVER['HTTP_USER_AGENT']; // get the user agent value - this should be cleaned to ensure no nefarious input gets executed

    if (preg_match('/ipod/i',$user_agent)||preg_match('/iphone/i',$user_agent)){
      return true;
    }
    if  (preg_match('/android/i',$user_agent)){
      return true;
    }
    return false;
  }

  /* Description
   *   use smarty in php, but some global support tmpl support should be added
   *   smarty read 'fragment' of HTML, but fewer HTML may be better...
   * Parameter:
   *    single parameter:
   *      full path to template
   *    multiple parameters:
   *      contact path to form the full path
   * TODO:
   *   sitewise template variable support
   */
  function load_tmpl(){
  	$arg_count = func_num_args(); // number of argument passed
  	$this->tmpl_assign('self_url', $this->param('SELF_URL'));
  	$this->tmpl_assign('TMPL', $this->param('TMPL'));
  	if ($arg_count == 1){
  		$temp = func_get_arg(0);
		  $tmplfile = VIEW_PATH.'/'.$temp;
		  if ($this->is_mobile_device()){
		  	$ext_pos =  strrpos($tmplfile , '.') +1;
        $ext = substr($tmplfile, $ext_pos);
        $mobile_tmplfile = substr_replace($tmplfile , 'mobile.'.$ext, $ext_pos);
        if( $this->tmpl_exists($mobile_tmplfile) ){
        	return $this->_load_tmpl($mobile_tmplfile);
        }
		  }
  		return $this->_load_tmpl($tmplfile);
  	}

  	$a_args = func_get_args(); // you can't just put func_get_args() into a function as a parameter
  	// call another function with variable number of parameter
    return $this->_load_tmpl(call_user_func_array(array($this,"fix_path"), $a_args));
  }

  /* Description:
   *   fix path issue on different OS.
   *   /home/cmsfactory/www   or \home\cmsfactory\www
   */
	function fix_path(){
		$a_args = func_get_args();
		//return realpath(implode('/', $a_args)); # realpath not just check, .. 
		return implode('/', $a_args);
	}

	/* Description:
	 *   change run_modes to access a 'list of string' directly.
	 */
	function run_modes(){
		
		$arg_count = func_num_args(); // number of argument passed
		if ($arg_count == 0){
		  return $this->_run_modes();
	  }

		$first_param = func_get_arg(0);
		if(is_string($first_param) && !empty($first_param)){ # convert to array

      $rm_function_mapping = array();
      for ($i=0; $i< $arg_count; $i++) {
      	$rm_name = func_get_arg($i);
      	if (method_exists($this, 'action_'.$rm_name)){
        	$rm_function_mapping[$rm_name] = 'action_'.$rm_name;
        } else {
          $rm_function_mapping[$rm_name] = $rm_name;
        }

      }      
      return $this->_run_modes($rm_function_mapping);
		}

		$second_param = func_get_arg(1);
		return $this->_run_modes($first_param, $second_param);
	}



  function teardown(){
    $this->db_disconnect(); // Disconnect when we're done
  }

/******************** End db related routine ********************/



/**************** Start input data processing function ****************/

  # get+set / get only interface:
  # 1. get + set: ?_param
  #    s_param: get set session parameter
  #    q_param: get get session parameter  
  #    
  # 2. get data interface:
  #    support default value and data type check
  #    get_q get from query (get and post)
  #    get_param()
  
  /*  get parameter from GET/POST
   */
	function get_q($name, $default_value='', $data_type=''){
    return $this->get_param('q', $name, $default_value, $data_type);
	}


	/*
	 * Name of parameter to path info mapping
	 * path info in fomat of /action(position 0)/position1/position2/ ,
	 * action action is not required to define, 0 by default
	 * @param hash $h_name_position 
	 *      typical $h_name_position : array(array('id'=>1)), 
	 *              array('oid'=>1,'topid'=>2,id=>3)
	 *              array('oid'=>1,id=>2)
	 *              array('topid'=>1,id=>2)
	 */
	function set_path_info_position($h_name_position){
		$this->_PATH_INFO_POSITION_MAPPGING =& $h_name_position;
	}

	/* This function get value from path info by position number,
	 * The name ane position relation is defined by 
	 *  set_path_info_position, which is typically called in setup()
	 * 
	 *  Position 0 is always 'action'
	 *  @return the value in corresponding position
	 */
	function path_info_param($position, $default_value='', $data_type=''){
		$value = $this->path_info($position);
		if ( empty_string($value)     ) return $default_value;
		if ( empty_string($data_type) ) return $value;
		return sanitizeOne($value, $data_type);
	}


	/*
	 * @param string $data_type pls refer to sanitizeOne()
	 */
  function get_param($sequence, $name, $default_value='', $data_type=''){
    $result = '';
    $sequence_len = strlen($sequence);
  	for ($i=0; $i<$sequence_len; $i++){
  		switch($sequence[$i]){
  			case 'q': // http POST & http GET
		      $result = isset($this->_CGI_APP_REQUEST[$name]) ? $this->_CGI_APP_REQUEST[$name] : NULL;
//		      echo "xxx $result";
		      break;
  			case 'p': // Path info
  			  $h_pi_position = $this->_PATH_INFO_POSITION_MAPPGING;
          $result = isset($h_pi_position[$name]) ? $this->path_info($h_pi_position[$name]) : NULL;   // check if we have name to id mapping,
          break;
  			case 's': // Session
  			  $result = $this->s_param($name);
  			  break;
  			default:
  			  break;
  		}//end switch
  		if (!empty_string($result)) break;
  	}//end for
/*
  	if (!empty_string($result)){
  		if (!empty_string($data_type))
		    return sanitizeOne($result, $data_type);
  		else 
  		  return $result;
  	}
  	return $default_value;
*/
  // by terry, cliff: really necessary to trim??
	if (is_string($result) ) $result = trim($result);			// string trim
 	if(empty_string($result) && !empty_string($default_value)){
		return $default_value;
	}
 
	if (!empty_string($data_type)){
		return sanitizeOne($result, $data_type);
  	}else{ 
  		return $result;
	}
//by terry

  }


  function get_pi_q($name, $default_value='', $data_type=''){
    $this->errorlog("calling obsolete function get_pi_q\n");
    return $this->get_param('pq', $name, $default_value, $data_type);
  }  
  

  /* the original query function has been 'disabled', 
   *  This function will direct access  _CGI_APP_REQUEST
   * 
   */
  function q_param(){
    
    // Determine how many arguments were sent
    $numargs = func_num_args();
    if (2 < $numargs) {
        // Bad number of arguments passed to q_param
        self::carp("Too many arguments sent to q_param()");
        return false;
    } elseif (0 == $numargs) {
        return $this->_CGI_APP_REQUEST;
    } elseif (1 == $numargs) {
        // One argument passed
        $data = func_get_arg(0);             
        if (is_string($data)) {                        	
            // Scalar arument passed -- return the request value associated
            // with it
            if (isset($this->_CGI_APP_REQUEST[$data])) {
                return $this->_CGI_APP_REQUEST[$data];
            } else {           	
                return false;
            }
        } elseif (!is_array($data)) {
            // Object or non-array sent as argument -- error!
            self::carp("Bad argument(s) sent to q_param()");
            return false;
        } elseif (!self::is_assoc_array($data)){ # pls not, associative array is also array
            $a_result = array();
            foreach ($data as $key) {
              array_push($a_result, $this->_CGI_APP_REQUEST[$key]);
            }
            return $a_result;
        }

        // We have an associative array; set each key/val pair accordingly
        // in the _CGI_APP_REQUEST array
        $params = array();
        foreach ($data as $key => $val) {
          $this->_CGI_APP_REQUEST[$key] = $val;
        }
    } else {
        // two arguments passed; assume key/val pair
        $key = func_get_arg(0);
        if (empty($key) || !is_string($key)) {
            self::carp('Bad key passed to s_param()');
            return false;
        }

        // Set key and val
        $val = func_get_arg(1);
        // Otherwise, set the value
        $this->_CGI_APP_REQUEST[$key] = $val;
    }

    return true;
  } // end q_param

  
   
	/*
	 * check if key exists in hash or return default value
	 */
	function array_key_exists_or_default( $key, $hash, $default_value){
		return array_key_exists($key, $hash) ? $hash[$key] : $default_value;
	}

/******************** End input data processing ********************/
  
  function errmsg($message, $button=''){
	  $this->tmpl_assign('title', 'Sorry! Application Error');
	  $this->tmpl_assign('body',  ''.$message);	  
	  return $this->load_tmpl('error.html');  	
  }



/*********************************************************
 * DB
 *********************************************************/
	/* Description:
	 *   db related stuff... any plug-in or helper approach?
	 *
	 * @param : rw_mode : for read / write separation, default read only
	 * @param : a particular db this app wanted to connect to
	 * @param : the default read write connection is supported in site_config.php
	 *          when connecting to db on another server, another connection setting can be provided
	 *          array($dbh_rost, $dbname, $dbuser, $dbpasswd)
	 *
 	 * Todo:
	 *   seperate read / write mysql handler
	 *   persistent db connection support (needed on cgi(N). fastcgi mode(probably Y), mode_php(probably Y))
	 *
   * Exception Error Code:
   *  30-39
	 * Reference:
	 *   http://pear.php.net/manual/en/package.database.db.intro-fetch.php
	 *   About db module comparision:
	 *     http://joseph.randomnetworks.com/archives/2006/04/04/php-database-functions-vs-peardb-vs-adodb/
	 *   About persistent connection
	 *   http://www.mysqlperformanceblog.com/2006/11/12/are-php-persistent-connections-evil/
	 *   http://php.net/manual/en/features.persistent-connections.php
   */
  function db_connect($rw_mode='read', $app_dbname='', $db_setting=array()){		
  	#if (isset($this->_DB_OBJECT[$rw_mode]) ){
  	#  $db_obj = $this->_DB_OBJECT[$rw_mode];	
  	#} else {
  		$app_dbname = empty_string($app_dbname)? $this->param('database'):$app_dbname;
      try{
  		  $db_obj = new DBI($rw_mode, $app_dbname, $db_setting);
  		  #$this->_DB_OBJECT = $db_obj;
  		  return $db_obj->connect(); # fix it later, reuse all db connection at least in the same PHP.
		  } catch (Exception $e){
			  throw new CgiappErrorException(31, 'Failed to connect to db:'.$e->getMessage());  	
		  }
	  #}
	  #return $db_obj->connect();
	  #return $db_obj;
	  //terry modify


	}


  function db_disconnect(){  	
  	#$db_obj = $this->_DB_OBJECT;
  	#if (isset($db_obj)) $db_obj->disconnect();
  }



  /*
   * default pattern
   *     app.php/list
   *     app.php/list/$oid
   *     app.php/list/$topid
   *     app.php/list/$oid/$topid
   *
   */
  function redirect_default_action($h_param = array()){
  	return $this->redirect($this->self_url($h_param));
  }
  
  function self_url($h_param = array()){
  	$a_path_info = array();
  	if (isset($h_param['action'])){
  	  array_push($a_path_info, $h_param['action']);
  	} else {
  		array_push($a_path_info, $this->start_mode());
  	}
  	
  	if (isset($h_param['oid']))    array_push($a_path_info, $h_param['oid']);
  	if (isset($h_param['topid']))  array_push($a_path_info, $h_param['topid']);
  	return $this->param('SELF_URL').'/'.implode('/', $a_path_info);
  }
  
  /* Gather message (warning / error) from app.
   *  and can display it on cgiapp_postrun phase
   *  @param no argument: get all gathered message
   *  @param 1 argument : add message
   */
  function message(){
    $numargs = func_num_args();
  	if (2 <= $numargs) {
      self::carp("Too many arguments sent to message()");
      return false;  		
    }	elseif (1 == $numargs) {
  		$message = func_get_arg(0);
  		$a_msg = $this->param('#MESSAGE#');
  		if (empty($a_msg) || !is_array($a_msg)){
  			$this->param('#MESSAGE#', array($message));
  	  } else {
  	  	array_push($a_msg, $message);
  	  	$this->param('#MESSAGE#', $a_msg);
  	  }
  	  return;
    }
    # no argument
    $a_msg = $this->param('#MESSAGE#');
  	if (is_array($a_msg)){  		
  		return $a_msg;
  	}	
  	
  	return array();
  }
 


/*********************************************************
 * html component supporting 
 * more db function can be found in dbUtil.php
 *********************************************************/
/* Description
 *   create the associative array (an ordered list) from a table, typicall for building html component
 *   $dbh_r: dbh read only handle
 */
function create_table_array($dbh_r, $table_name,  $index_field, $top_field, $value_field, $where_condition=''){
	echo("create_table_array - depreciated, pls use create_lookup_array");
	return $this->create_lookup_array($dbh_r, $table_name,  $index_field, $top_field, $value_field, $where_condition='');
} 

function create_lookup_array($dbh_r, $table_name,  $index_field,  $value_field, $where_condition=''){
  $a_result= array();

  $sqlstr = "select $index_field, $value_field from $table_name $where_condition";
  if (! preg_match('/order/i', $where_condition)){
    $sqlstr .= " order by $index_field" ;
  }
  //PDO  syntax  
  $sth = $dbh_r->prepare($sqlstr);
  $sth->execute();
  while ($row = $sth->fetch(PDO::FETCH_NUM)){
  	$a_result[$row[0]]=$row[1];
  }

  return $a_result;
}

/*
 * create multi-level lookup, for mlist - html component
 */
function create_ml_lookup_array($dbh_r, $table_name, $index_field, $top_field, $value_field, $where_condition='', $add_root_node=0){
  $a_result= array();

  $sqlstr = "select $index_field, $top_field, $value_field from $table_name $where_condition";
  if (! preg_match('/order/i', $where_condition)){
    $sqlstr .= " order by $index_field" ;
  }

  //PDO  syntax  
  //$sth = $dbh_r->prepare($sqlstr);
  //$sth->execute();
  $result = $dbh_r->query($sqlstr);
  try{
  	if ($result){
	    while ($row = $result->fetch(PDO::FETCH_NUM)){
	    	array_push($a_result, $row[0],$row[1],$row[2]); # array of array or want to flatten it? 
      }
      
      if ($add_root_node == 1){
      	array_unshift($a_result, '0', '-1', 'Root');
      }
      
    }
  } catch (Exception $e){
	  	throw new CgiappErrorException(101, 'process_select_result(): Error in SQL?:'.$e->getMessage());  	
  }

  return $a_result;
}



/**
 * $dbh_r: dbh read only
 * return array triple ( $id1, $top1, $field1, $id2, $top2, $field2)
 */
function create_table_orderedlist_for_catree
  ($dbh_r, $table_name, $array_index_field, $top_fieldname, $value_fieldname, $where_condition=''){
  $a_result= array();

  $sqlstr = "select $array_index_field, $top_fieldname, $value_fieldname from $table_name $where_condition";
  if (! preg_match('/order/i', $where_condition)){
    $sqlstr .= " order by $array_index_field" ;
  }

  $sth = $dbh_r->prepare($sqlstr);
  $sth->execute();
  while ($row = $sth->fetch(PDO::FETCH_NUM)){
  	array_push($a_result, $row[0], $row[1], $row[2]);
  }

  return $a_result;
}



/*********************************************************
 * common routine
 *********************************************************/
  function errorlog(){
  	$numargs = func_num_args();
  	
  	if (1 == $numargs) {
  		$name ='cgiapp';
  		$message = func_get_arg(0);
    } else {
  		$name = func_get_arg(0);
  		$message = func_get_arg(1);
    }
    $now = date("m-d-y H:i:s");
    $path = $this->fix_path($this->param('LOG'),$name.'.log');
    //$this->errorlog('path '.$path);
    AppendFile($path, "$now\t$message\n");  
  }
  
  function log_error($name, $msg){
    $now = date("m-d-y H:i:s");
    $path = $this->fix_path($this->param('LOG'),$name.'.log');
    //$this->errorlog('path '.$path);
    AppendFile($path, "$now\t$msg\n");
  }

  /* Description
   *   Redirect to a page,
   * IMPORTANT:
   *   about php.ini 
   *   need to set output_buffering [ = 4096 ]
   */
  function redirect($url){
    $this->header_type('redirect');
    $this->header_props(array('url'=>$url));
    return "Redirecting to page: $url";
  }

  /* standard error page
     todo:
        custom_errmsg_info, custom_errmsg, custom_info, errmsg, info,
        db_errmsg => change to better support of db error... corrupted table??
  */

/*********************************************************
 * CORE ROUTINE !! Very important
 *********************************************************/
	/*
	 * typically for list / edit / view acton
   * Exception Error Code:
   *  50-59
   *
   *
   * return
   *   $aoh_data_final : array($fieldname => value,...)
   *   $a_pagelist :     pagelist generatd
   *   $a_id_column_selected : id of row being processed
   * Note:
   *   other optional parameter in $h_feature
   *   - #GET_SELECTED_ID#    : it will return the third paramter, an array of selected id
   *     
	 */
	function process_select_result($dbh_r, $sqlstr, $h_feature){
		$get_selected_id = get_hash_param($h_feature, '#GET_SELECTED_ID#', 0);
	  $multiple_update = 0;
	  $key_column_name = 'id';
	  $a_id_column_selected = array();

    //always assume table use 'id' field as primary key column if mulitple update mode
	  $key_column_name = get_hash_param($h_feature, '#KEY#', 'id');
	  
	  # edit multiple row
	  if (isset($h_feature['#MULTIPLE#']) && $h_feature['#MULTIPLE#'] == 1){
	    $multiple_update = 1;
	    
	  }

	  $a_pagelist = array();  // hold the generated pagelist
	  $total = -1;                 // total number of match in sql.
	  $pagelist_required = 0;

	  $page = 0;
	  $pagesize = 0; // to be calculated
	  $first_pagesize = 0;
	  $first_pageoffset = 0;

    $limit_base = 0;
    $limit_pagesize = 0; 
	  // Check if we use page list
	  if (array_key_exists('#PAGELIST#', $h_feature)){
	  	#usage of SQL_CALC_FOUND_ROWS and FOUND_ROWS()
	  	#To obtain this row count, include a SQL_CALC_FOUND_ROWS option in the SELECT statement, and then invoke FOUND_ROWS() afterward: 
	  	#SELECT SQL_CALC_FOUND_ROWS * FROM tbl_name WHERE id > 100 LIMIT 10;
      #SELECT FOUND_ROWS();

	  	$pagelist_required = 1;

	    $page             = get_hash_param($h_feature, '#PAGE#',     $this->get_q('page',  1));
	    $pagesize         = get_hash_param($h_feature, '#PAGESIZE#', 10);
	    $first_pagesize   = get_hash_param($h_feature, '#FIRSTPAGESIZE#',    $pagesize);
	    $first_pageoffset = get_hash_param($h_feature, '#FIRSTPAGEOFFSET#',  0);
	    #list ($limit_base, $limit_pagesize) = Pagelist::limit_base($page, $pagesize, $first_pagesize, $first_pageoffset);
	    #$sqlstr .= " LIMIT $limit_base, $limit_pagesize";
      #
	    #// NOTE: THIS IS A MYSQL HACK FOR SELECT STATEMENT WITH limit , pls reimplement for other db
	    ##To obtain this row count, include a SQL_CALC_FOUND_ROWS option in the SELECT statement, and then invoke FOUND_ROWS() afterward: 
	    #$sqlstr = preg_replace('/^\s?select/i', 'SELECT SQL_CALC_FOUND_ROWS', $sqlstr);
	    $pagelist_class   = get_hash_param($h_feature, '#PAGELIST_CLASS#',  $this->param('PAGELIST_CLASS'));
	    
	    if (! method_exists($this, 'get_pagelist_obj')){
	    	#require_once('CGIApplication/Pagelist.class.php');
	      $pagelist_obj = new Pagelist();
	    } else {
	    	$pagelist_obj = $this->get_pagelist_obj();
	    }
	    list ($limit_base, $limit_pagesize) = $pagelist_obj->limit_base($page, $pagesize, $first_pagesize, $first_pageoffset);
	    $sqlstr .= " LIMIT $limit_base, $limit_pagesize";

	    // NOTE: THIS IS A MYSQL HACK FOR SELECT STATEMENT WITH limit , pls reimplement for other db
	    #To obtain this row count, include a SQL_CALC_FOUND_ROWS option in the SELECT statement, and then invoke FOUND_ROWS() afterward: 
	    $sqlstr = preg_replace('/^\s?select/i', 'SELECT SQL_CALC_FOUND_ROWS', $sqlstr);	    
	  }


	  /**************************************************/
	  // ???? will it have big memory issue when selecting large amount of data to hash???

	  //$aoh_data = $dbh_r->getAll($sqlstr, array(), DB_FETCHMODE_ASSOC);
    #$result = $dbh_r->query($sqlstr);
    $sth = $dbh_r->prepare($sqlstr);
    $sth->execute();
    $result = $sth->fetchAll(PDO::FETCH_ASSOC);
    $aoh_data_final = array();
    $record_count = 0;
    try{
    	if ($result){
		    //while ($h_row = $result->fetch(PDO::FETCH_ASSOC)){
		    foreach ($result as $h_row){
			    if ($total == -1){
			    	# For a SELECT with a LIMIT clause, the number of rows that would be returned were there no LIMIT clause
			    	$temp_q = $dbh_r->query('SELECT FOUND_ROWS()');
			    	$temp = $temp_q->fetch();
			    	#echo 'xx'.gettype($temp);
			    	$total = array_pop($temp);			    	
			    	#echo "total => $total<BR>";
			    }
			    //$n_fields = -1;
			    //foreach ($aoh_data as $h_row){ // row of hash
			  	$h_row_final = array('record_count'=> $limit_base + $record_count);
			  	$h_row_function_final = array();
			  	// a general row function to modify the result,
			  	// it is the first function to run, before any other...
			  	if ( array_key_exists('#ROW_FUNCTION#', $h_feature)){
			  		$row_function = $h_feature['#ROW_FUNCTION#'];
			  		if (method_exists($this, $row_function)){
			  	    $h_row_function_final = $this->$row_function($dbh_r, $h_row);
			      }
			    }

			  	if ($get_selected_id==1){
			      array_push($a_id_column_selected, get_hash_param($h_row, $key_column_name, 0)); # selected index
			    }
			      
			  	//if ($n_fields == -1) $n_field = count($h_row);
			  	//$n_field = count($h_row);
			  	foreach ($h_row as $colname => $col_value){
			  		//echo "$colname => $col_value<BR>";


			      if ( ! array_key_exists($colname, $h_feature)){ // feature is not exist, direct assign
			        $h_row_final[$colname] = $col_value;
			      } else {
			      	#$this->errorlog("process_select_result: $colname - ".print_r( $h_feature[$colname]));
			        $temp_result = $this->process_feature($dbh_r, $h_row, $colname, $h_feature,
			                   $multiple_update, $h_row[$key_column_name]);
				  		if (self::is_assoc_array($temp_result)){
				  			foreach ($temp_result as $temp_k => $temp_v){
				  				$h_row_final[$temp_k] = $temp_v;
				  			}
				  		} else {
				  		  $h_row_final[$colname] = $temp_result;
				  	  }

			      }

			    } //  foreach ($h_row as $colname => $v)
	
          #$h_row_final = array_merge($h_row_final, $h_row_function_final); // data from row function is final
          #http://www.bitbybit.dk/carsten/blog/?p=203
          if(!empty($h_row_function_final)){
            foreach($h_row_function_final as $rowfunc_key => $rowfunc_value) { # faster as h_row_function_final usu have a few data
              $h_row_final[$rowfunc_key] = $rowfunc_value;
            }
          }
			    array_push($aoh_data_final, $h_row_final);
			    
			    $record_count++;
			  } // while fetchrow / foreach ($aoh_data as $h_row)
		  }
	  } catch (Exception $e){
	  	throw new CgiappErrorException(51, 'process_select_result(): Error in SQL?:'.$e->getMessage());
	  }

	  /**************************************************/

	  // handle pagelist here
	  if ($total > 0 && array_key_exists('#PAGELIST#', $h_feature)){       // page list enabled
	    $h_pagelist_param = array('page'   => $page,
	                     'pagesize'        =>$pagesize,
	                     'total'           =>$total,
	                     '#FIRSTPAGESIZE#' => $first_pagesize,
	                     '#OFFSET#'        => $first_pageoffset,
	                    );
	    if (array_key_exists('#STATIC#', $h_feature) && $h_feature['#STATIC#'] == 1){  // static pagelist
	      $h_pagelist_param['base_url']  = $h_feature['#STATIC_PAGENAME#'];
	      $h_pagelist_param['is_static'] = 1;
	      $optional_params = array('#PAGELISTFORMAT#','#UNIT#',);
	      foreach ($optional_params as $optional_param){
	        if (array_key_exists($optional_param, $h_feature)) $h_pagelist_param[$optional_param] = $h_feature[$optional_param];
	      }
	    } else {
	    	if (array_key_exists('#BASEQUERYSTR#', $h_feature)){    # stop support  '#BASEQUERYSTR#'
	      		  $h_pagelist_param['querystr'] = $h_feature['#BASEQUERYSTR#'];
		    } else {
		    #	$extra_query_str = get_hash_param($h_feature, '#EXTRAQUERYSTR#', '');
		    #	$h_pagelist_param['querystr'] = $this->param('CGI_URL').$this->param('this_cgi').'?action='.$this->get_current_runmode().$extra_query_str;
		    	$extra_query_str = get_hash_param($h_feature, '#EXTRAQUERYSTR#', '');
		    	$h_pagelist_param['base_url'] = $this->param('CGI_URL').$this->param('this_cgi');
		    	$h_pagelist_param['query_str'] = 'action='.$this->get_current_runmode().$extra_query_str;		    	
		    }

	      $optional_params = array('#PAGELISTFORMAT#','#UNIT#');
	      foreach ($optional_params as $optional_param){
	        if (array_key_exists($optional_param, $h_feature)) $h_pagelist_param[$optional_param] = $h_feature[$optional_param];
	      }
	    }

	    $pagelist_obj->set_param($h_pagelist_param);
	    $a_pagelist = $pagelist_obj->gen('1,2,3');
	  } else {
	  	if (array_key_exists('#PAGELIST#', $h_feature)){ # if pagelist has defined
	  	  $a_pagelist = $pagelist_obj->gen_empty();
	    }
	  }

	  return array($aoh_data_final, $a_pagelist, $a_id_column_selected);
	} //process_select_result


	/* dbh read # show be for read only
	 *   $multiple: 1: if in multiple update mode
	 *   $key : current id value for this row (no need in non-multiple update)
	 */
	function process_feature($dbh_r, $h_row, $colname, $h_feature, $multiple=0, $key=0 ){
	  $default_value      = isset($h_row[$colname])? $h_row[$colname]:'';
	  $feature_type_mixed = $h_feature[$colname];

	  if ( is_array($h_feature[$colname]) ){
	  	if (! isset($feature_type_mixed[2]))  $feature_type_mixed[2]='';
	  	if (! isset($feature_type_mixed[3]))  $feature_type_mixed[3]='';
	    list ($type, $name, $mixed, $extra_param) = $feature_type_mixed;
	    
	  } elseif ( is_string($feature_type_mixed) ){
	  	$type = $feature_type_mixed;
	  }

	  if ($multiple == 1) $name = $name.'_'.$key ;

	  if ($type == 'skip')  return '';  // skip this fields
	  //$this->errorlog('process_feature type '.$type);
	  if ( preg_match('/^h:/',  $type) )  return  gen_html_component($type, $default_value, $name, $mixed, $extra_param) ; # $mixed is array
	  if ( preg_match('/^ny:/', $type) )  return  gen_html_component($type, $default_value, $name, $mixed, $extra_param) ; # $mixed is array
	  if ($type == 'f:func'){
	  	if (is_string($mixed)){
	      return $mixed($default_value);    # $mixed is the function name this time
	    }
	    return $mixed($default_value);
	  }
	  if ($type == 'f:string') return $mixed;    # $mixed is the string

	  return  gen_html_component($type, $default_value, $name, $mixed, $extra_param) ; # $mixed is array

	//  if ($type == 'f:func_row'){
	//    &$mixed($self, $txt_ref, $sth, $colname, $row_ref, $position, $feature_ref, $dbh_r) ;    # $mixed is func_ref
	//  }

	//  if ($type == 'f:func_null') return; # do nothing and return
	//

	//
	//  if ($type eq 'f:func_self'){
	//    &tagreplace($txt_ref, '#'.uc($colname).'#', &{$mixed}($self, $default, $name) );    # $mixed is func_ref
	//  }
	//
	//  if ($type eq 'catree'){
	//  	&tagreplace($txt_ref, '#'.uc($colname).'#', $this->gen_html_catree($default, $name, $mixed) );  # $mixed is array_ref
	//  }

	} // end process_feature

#########################################################################

  /* Descriptioin
   *   return $a_column : column from $table
   */
  function get_column_name($dbh_r, $table){
    return get_column_name($dbh_r, $table); # implemented in dbUtil.phpget_column_name($dbh_r, $table)
  }


	/**
	 * Update data to a table.
	 *   data can be from HTML form post or hash data.
	 *   it will insert available column only (check against available field in table)
	 *   this function is safe from sql injection
	 *
	 * refrence : http://www.sitepoint.com/forums/showthread.php?t=497257
	 * <code>
	 *   $this->update_table($dbh, $this->param('table'));
	 * </code>
	 *
   * Exception Error Code:
   *  60-69   
	 * @param  PDO $dbh db PDO db WRITE hander
	 * @param  string $table name of the table
	 * @param  mixed $mixed_data if equal 'HTTP_POST', it will get data from http post.
	 *         or as hash like  array('id'=>1, 'name'=>'Local', 'cname'=>'local');
	 * @param  hash $h_feature '#KEY#' => [name of pk field], 'field_name' => 'boolean', 'field_name2' => 'skip',
	 */
	function update_table($dbh_w, $table, $mixed_data='HTTP_POST', $h_feature=array()){
	  if ($mixed_data=='HTTP_POST'){
	    return update_table($dbh_w, $table, $this, $h_feature);
	  } else {
	    return update_table($dbh_w, $table, $mixed_data, $h_feature);
	  }
	}



  /* Description:
	 *   name of data will be in format of: (id1, field1_id1, field2_id1, id2, field1_id2, field2_id2)
	 *
	 * this function assume data to be pass in in the following format:
	 * For fields to be updated, if its name are "field1" and "field2" etc,
	 * its name in form will be "field1_".$key and "field2_".$kdey respectiveily
	 * for example if the key are 1, 2 and field are name and phone,
	 * it need to have the following fields in the form:
	 * name='id'       value = '1'
	 * name='name_1'   value = 'my name'
	 * name='phone_1'  value = '108'
	 * name='id'       value = '2'
	 * name='name_2',  value='your name'
	 * naem='phone_2', value='109'
	 *
	 * @all_key = (1, 2);
	 *
   * Exception Error Code:
   *  60-69  
	 * $dbh is the dbh write by default,
	 */
	function update_table_multiple_row($dbh_w, $table, $mixed_data='HTTP_POST', $h_feature=array()){  
    if ($mixed_data=='HTTP_POST'){
	    return update_table_multiple_row($dbh_w, $table, $this, $h_feature);
	  } else {
	    return update_table_multiple_row($dbh_w, $table, $mixed_data, $h_feature);
	  }
  }

  /*
   * check for 'id'
   * delete single record support query string / path_info
   * but delete multiple support only POST
   * Exception Error Code:
   *  70-79  
   */
  function delete_from_table($dbh_w, $table, $mixed_data='HTTP_POST' ,$h_feature=array()){
    if ($mixed_data=='HTTP_POST'){
	    return delete_from_table($dbh_w, $table, $this, $h_feature);
	  } else {
	    return delete_from_table($dbh_w, $table, $mixed_data, $h_feature);
	  }
  }
  


	/*
	 * Description: delete / hidden
	 * HTTP_POST:
	 *   it will look for 'mselected_id' (if key_name is id) for delete
	 *   in HTML, it should be a checkbox with name='mselected_id[]'
   * Exception Error Code:
   *  70-79 
	 */
  function delete_multiple_from_table($dbh_w, $table, $mixed_data='HTTP_POST' ,$h_feature=array()){
    if ($mixed_data=='HTTP_POST'){
	    return delete_multiple_from_table($dbh_w, $table, $this, $h_feature);
	  } else {
	    return delete_multiple_from_table($dbh_w, $table, $mixed_data, $h_feature);
	  }
  }

  
  /* for showadd action
   * $h_row: default value show in input box
   */
  function process_showadd_feature($dbh_r, $h_feature, $h_row=array()){
    $h_row_final = array();
    $h_row_function_final = array();
        
    // a general row function to modify the result,
		// it is the first function to run, before any other...
		if ( array_key_exists('#ROW_FUNCTION#', $h_feature)){
			$row_function = $h_feature['#ROW_FUNCTION#'];
			if (method_exists($this, $row_function)){
			  $h_row_function_final = $this->$row_function($dbh_r, $h_row);
			  #print_r($h_row_final);
			}
			unset($h_feature['#ROW_FUNCTION#']);
		}
 
    
    foreach ($h_row as $colname => $default_value){
      $h_row_final[$colname] = $default_value;      
    }
    
  	foreach ($h_feature as $colname => $col_value){
        $h_row_final[$colname] = $this->process_feature($dbh_r, $h_row, $colname, $h_feature);
	  }
	  
	  if(!empty($h_row_function_final)){
      foreach($h_row_function_final as $rowfunc_key => $rowfunc_value) { # faster as h_row_function_final usu have a few data
        $h_row_final[$rowfunc_key] = $rowfunc_value;
      }
    }
	  return $h_row_final;
  }


	/* Description: Adding data to table, from web or from an associative array
	 * @param $mixed_data:  'HTTP_POST' , data from form post or
	 * @param $mixed_data:  assosciative array of data 
   * Exception Error Code:
   *  80-89
	 */
	function add_to_table($dbh_w, $table, $mixed_data='HTTP_POST' ,$h_feature=array()){
    if ($mixed_data=='HTTP_POST'){
	    return add_to_table($dbh_w, $table, $this, $h_feature);
	  } else {
	    return add_to_table($dbh_w, $table, $mixed_data, $h_feature);
	  }
  }

	
  
  /*
   * Exception Error Code:
   *  80-89
   * added with default key (id) is not supported?
   * $mixed_data: 
   *   1. HTTP_POST: a) add (no id specified) b) add with specified ID
   *   2. a) add (no id specified)
   *      array('name_1'=>'Fashion', 'cname_1'=>'Fashion','name_2'=>'Beauty',  'cname_2'=>'Beauty')
   *   3. b) add with specified ID
   *   array('id'=>array(4,5), 'name_4'=>'Fashion', 'cname_4'=>'Fashion', 'name_5'=>'Beauty',  'cname_5'=>'Beauty')
   */
	function add_to_table_multiple_row($dbh_w, $table, $mixed_data='HTTP_POST', $h_feature=array()){
	  if ($mixed_data=='HTTP_POST'){
	    return add_to_table_multiple_row($dbh_w, $table, $this, $h_feature);
	  } else {
	    return add_to_table_multiple_row($dbh_w, $table, $mixed_data, $h_feature);
	  }
  }
  



/* typical feature : (id =>'auto_increment', 'addtime' => 'now', lastmodify=>'now');
 *
 * Exception Error Code:
 *  90-99
 * @param: $dbh_w db write handle
 * @param: $copy_key: the copying parent_key
 */ 
function copy_row_by_parent_key($dbh_w, $table, $parent_key_name, $copy_key, $new_key, $h_feature){    
  copy_row_by_parent_key($dbh_w, $table, $parent_key_name, $copy_key, $new_key, $h_feature);
}


/*
 * the key must be auto_increment field
 * Exception Error Code:
 *  80-89
 */
function copy_row_by_key($dbh_w, $table, $key_name, $copy_key, $h_feature){
  copy_row_by_key($dbh_w, $table, $key_name, $copy_key, $h_feature);
}










/************** start gdb / memcached support *******************/
# to be implemented 




/*********************************************************
 * END CORE ROUTINE
 *********************************************************/
 function text_to_array($text){
   return preg_split('/$\R?/m', $text);
 }
   
  # return all files in a folder (it won't check any subfolder)
  function get_files_in_folder($folder, $include_subfolder){
    if ($include_subfolder){
      # not implemented
      return array();
    }
    if (substr($folder, -1) != '/') $folder .= '/';   # add trailing slash
    $a_files = array();
    if ($handle = opendir($folder)) {
      while (false !== ($file = readdir($handle))) {
        if ($file == "." || $file == "..") next;
        if (! is_dir( $folder.$file )){  # only accept file
            array_push($a_files, $folder.$file);
        } 
      }
      closedir($handle);
    }
    return $a_files;
  }
 
 
  /* Handle application errors during the run mode
   *
   * The $errstr is logged to internal log file,
   * User error message is obtained from other file lookup.
   * ??? more? capture which file it caused ?
   *
   */
	function exception_handler($errno, $errstr){
	  //$this->errorlog("exception_handler: Error number: $errno - ".$errstr);
	  //$msg  = "The following error occurred: $errstr";
	  //$msg .= "<BR/>DB Connection Count:".$this->db_connection_count.'<BR/>';
	  //$this->_ERROR_BODY = $msg;
	  $this->tmpl_assign('title', 'Sorry! Application Error (Error Code:'.$errno.')');
	  $this->tmpl_assign('body',  'Error message:<BR/>'.$errstr);	  
	  #echo 'tmpl path'.$this->param('TMPL');
	  return $this->load_tmpl('error.html');
    //return $msg; // Format the error page
	}
  
  /*
   */
  function action_trap(){
  	return 'Reach an un-supported action.';
  }  
  
  
	/** 上傳圖片	//terry
	* @param String	$source		file 或 url
	* @param Array	$param		上傳參數
	* @return Array	$flag $result
	*/
	function upload_photo_imp($source,$param=array()){
		$type = isset($param['type']) && !empty($param['type'])? $param['type'] : 'jpg';
		$size = isset($param['size']) && !empty($param['size'])? $param['size'] : 819200;	//默認限制800K
		$path = isset($param['path']) && !empty($param['path'])? $param['path'] : 'temp';
		$filename = isset($param['filename']) && !empty($param['filename'])? $param['filename'] : date('YmdHis').time();
		$convert_to = isset($param['convert_to']) && !empty($param['convert_to'])? $param['convert_to'] : '';
		$max_dim = isset($param['max_dim']) && !empty($param['max_dim'])? $param['max_dim'] : '500x500';
		$mode = isset($param['mode']) && !empty($param['mode'])? $param['mode'] : 0;
		$thumb = isset($param['thumb']) && !empty($param['thumb'])? $param['thumb'] : array();

		require_once 'ImageUpload.php';
		$upload = new ImageUpload($source,$type,$size);
		
		$relative_output_path = '/upload/'.$path.'/';
		$absolute_output_path = UPLOAD_PATH.'/'.$path.'/';
		if(!file_exists($absolute_output_path) || filetype($absolute_output_path)!='dir'){
			mkdir($absolute_output_path,0777);
			chmod($absolute_output_path,0777);
		}

		$h_setting = array();
		for($i=0,$max=count($thumb)+1; $i<$max; $i++){
			if($i==0){	//原圖
				$h_setting[$i] = array('convert_to'=>$convert_to,'max_dim'=>$max_dim,'mode'=>$mode,'filename'=>get_upolad_image_path(0,$i,array('default_filename'=>$filename)));
			}else{		//縮略圖
				$h_setting[$i] = array('convert_to'=>$convert_to,'max_dim'=>$thumb[$i-1]['max_dim'],'mode'=>$thumb[$i-1]['mode'],'filename'=>get_upolad_image_path(0,$i,array('default_filename'=>$filename)));
			}
		}

		list($flag, $a_processed_image) = $upload->save($absolute_output_path, $h_setting);
		if ($flag != 0){
			return array($flag,$a_processed_image);	//返回錯誤信息
		}else{
			for($i=0,$max=count($a_processed_image); $i<$max; $i++){
				$a_processed_image[$i][2] = $relative_output_path.$a_processed_image[$i][2];
			}
		}
		return array(0,$a_processed_image);
	}
  
	/** ajax返回數據
	* @param Mixed	$data	內容
	* @param Array	$config	設置
	*/
	function ajaxReturn($data,$config=array()){
		$defaultConf = $this->param('DATARETURN');
		
		$param = array();
		$param['type'] = isset($config['type'])? $config['type'] : $defaultConf['type'];
		$param['retcode'] = isset($config['retcode'])? $config['retcode'] : $defaultConf['retcode'];
		$param['msg'] = isset($config['msg'])? $config['msg'] : '';
		$param['data'] = isset($data)? $data : '';
		$param['format'] = isset($config['format'])? $config['format'] : $defaultConf['format'];
		$param['xmlroot'] = isset($config['xmlroot'])? $config['xmlroot'] : $defaultConf['xmlroot'];

		$return_obj = getLibClass('DataReturn',$param);
		$return_obj->data_return();
	}


	/** iframe返回數據
	* @param Mixed	$data		內容
	* @param String	$callback	返回方法
	* @param Array	$config		設置
	*/
	function iframeReturn($data,$callback='',$config=array()){
		$defaultConf = $this->param('DATARETURN');

		$param = array();
		$param['type'] = 'IFRAME';
		$param['retcode'] = isset($config['retcode'])? $config['retcode'] : $defaultConf['retcode'];
		$param['msg'] = isset($config['msg'])? $config['msg'] : '';
		$param['data'] = isset($data)? $data : '';
		$param['callback'] = isset($callback)? $callback : $defaultConf['callback'];

		$return_obj = getLibClass('DataReturn',$param);
		$return_obj->data_return();
	}

} // end class
?>